﻿using System;
using System.Linq;
using System.Linq.Expressions;

namespace Affinity.Synthesis.ExpressionCells
{
    /// <summary>
    /// Provides the common base implementation of IExpressionCell for simple (non-composite) expression cells.
    /// </summary>
    internal abstract class ExpressionCellBase : IExpressionCell
    {
        /// <summary>
        /// Constructor accepting the atomic expression that this expression cell will represent.
        /// </summary>
        /// <param name="sourceExpression"></param>
        protected ExpressionCellBase(Expression sourceExpression)
        {
            _Output = new ExpressionCellValue(this);

            if (sourceExpression != null) _Description = sourceExpression.ToString().Replace("Param_0.", string.Empty);

#if DEBUG
            _SourceExpression = sourceExpression;
#endif
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="existing"></param>
        protected ExpressionCellBase(ExpressionCellBase existing)
            : this((Expression)null)
        {
            Metadata = existing.Metadata;
            _Description = existing._Description;

#if DEBUG 
            _SourceExpression = existing._SourceExpression;
#endif
        }

        /// <summary>
        /// Gets a description of this expression cell.
        /// </summary>
        public string Description
        {
            get { return string.IsNullOrWhiteSpace(_Description) ? GetType().Name : _Description; }
        }

        /// <summary>
        /// Gets or sets the ExpressionCellValues representing the inputs.
        /// </summary>
        public ExpressionCellValue[] Inputs
        {
            get { return (_Inputs); }
            set
            {
                foreach (var newInput in value) if (newInput == null) throw new ArgumentException("Inputs must all be non-null.");

                if (_Inputs != null) foreach (var input in _Inputs) UnhookInput(input);
                _Inputs = value;
                if (_Inputs != null) foreach (var input in _Inputs) HookInput(input);
                OnInputsChanged();
            }
        }

        /// <summary>
        /// Gets the metadata array. Decendant classes may also set the metadata array.
        /// </summary>
        public object[] Metadata
        {
            get { return _Metadata; }
            protected set { _Metadata = value; }
        }

        /// <summary>
        /// Gets the ExpressionCellValue representing the output.
        /// </summary>
        public ExpressionCellValue Output
        {
            get { return _Output; }
        }

        /// <summary>
        /// Defines an abstract method for creating a copy.
        /// </summary>
        /// <returns></returns>
        public abstract IExpressionCell Copy();

        /// <summary>
        /// Returns a description. (Overrides Object.ToString().)
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return Description;
        }

        /// <summary>
        /// Defines an abstract method, the concrete implementations of which perform the 'calculation' of this expression cell's output.
        /// </summary>
        /// <param name="inputs">The input values (not ExpressionCellValue objects, but their values).</param>
        /// <param name="hasValue">Reports whether this expression cell has a value</param>
        /// <param name="value">The 'calculated' output value, if hasValue is true. Is hasValue is false, value should be ignored.</param>
        protected abstract void CalculateOutput(object[] inputs, out bool hasValue, out object value);

        /// <summary>
        /// Called when the inputs have changed.
        /// </summary>
        protected virtual void OnInputsChanged()
        {
            OnValueDirty();
        }

        /// <summary>
        /// Called when the value of this expression cell is dirty and the output must be recalculated if possible.
        /// </summary>
        protected void OnValueDirty()
        {
            bool hasValue;
            object value;

            if (Inputs.Any(input => !input.HasValue))
            {
                hasValue = false;
                value = null;
            }
            else
            {
                CalculateOutput(Inputs.Select(input => input.Value).ToArray(), out hasValue, out value);
            }

            _Output.Set(hasValue, value);
        }

        /// <summary>
        /// Begins listening for a change in an input value.
        /// </summary>
        /// <param name="input"></param>
        private void HookInput(ExpressionCellValue input)
        {
            input.Changed += OnValueDirty;  
        }

        /// <summary>
        /// Stops listening for a change in an input value.
        /// </summary>
        /// <param name="input"></param>
        private void UnhookInput(ExpressionCellValue input)
        {
            input.Changed -= OnValueDirty;
        }

        private string _Description = null;
        private ExpressionCellValue[] _Inputs = null;
        private object[] _Metadata = new object[0];
        private ExpressionCellValue _Output = null;

#if DEBUG
        private Expression _SourceExpression = null;
#endif
    }
}
